データ競合 と Rust
from 項目17:状態共有並列実行には気を付けよう
借用ルール により、以下のような構造体のインスタンスを マルチスレッド で共有しようとしても コンパイルエラー となる
code:rs
pub struct BankAccount {
balance: i64,
}
code:rs
let mut account = BankAccount::new();
let _payer = std::thread::spawn(move || pay_in(&mut account));
let _taker = std::thread::spawn(move || take_out(&mut account));
∵ 可変参照 は同じスコープで 1 つしか許されていないため
コンパイルを通すためには、同期機構である Mutex を用いる
他の言語(e.g. C++ の std::mutex)とは異なり、独立したオブジェクトではなくラッパとなっている
code:rs
pub struct BankAccount {
balance: std::sync::Mutex<i64>,
}
ロック を取得する Mutex::lock メソッドは Result<MutexGuard<T>, PoisonError<MutexGuard<T>>> を返す
MutexGuard は RAII パターン となっており、スコープの末尾が ドロップ されるとロックが解除される
Result でラップされているのは、ロックが Poisoning となる可能性があるため
Poisoning
スレッドが panic! を起こし、ロックが正常に解放されなかった状態
しかしほとんど起こり得ないのに加えて、起こったとしても即座にプログラムを停止した方が良いケースが多いので unwrap() とすることが多い
MutexGuard は Deref や DerefMut トレイトを実装しているため、内部値への プロキシ としての機能も持つ
code:rs
pub fn balance(&self) -> i64 {
let balance = *self.balance.lock().unwrap();
if balance < 0 {
panic!("** Oh no, gone overdrawn: {}", balance);
}
balance
}
code:rs
pub fn deposit(&mut self, amount: i64) {
*self.balance.lock().unwrap() += amount
}
pub fn withdraw(&mut self, amount: i64) -> bool {
let mut balance = self.balance.lock().unwrap();
if *balance < amount {
return false;
}
*balance -= amount;
true
}
warning.icon
BankAccount の balance を変更するにも関わらず、&mut self ではなく &self を受け取る
これは、借用ルール 上、複数の参照を 可変 にすることはできないため
内部可変性(項目8:参照型とポインタ型に慣れよう#6777c79075d04f000001a977)の一例
借用チェッカ が事実上コンパイル時から実行時に移っているが、その代わりに スレッド 間の同期機構が追加される
しかし、BankAccount の balance を Mutex でラップしても、生存期間 に関するエラーが発生する
code:rs
let mut account = BankAccount::new();
let _payer = std::thread::spawn(move || pay_in(&account));
let _taker = std::thread::spawn(move || take_out(&account));
∵ move により account が 1 つ目のクロージャにムーブし、クロージャの終了時にドロップされるため
これを回避するには、std::sync::Arc を用いると良い(Arc<Mutex<T>>)
code:rs
let account = std::sync::Arc::new(BankAccount::new());
account.deposit(1000);
let account2 = account.clone();
let _taker = std::thread::spawn(move || take_out(&account2));
let account3 = account.clone();
let _payer = std::thread::spawn(move || pay_in(&account3));
それぞれのスレッドは、参照カウンタ ポインタのコピーをクロージャにコピーして受け取る
参照されている BookAccount は参照カウントが 0 になった場合にのみ ドロップ される
以上を踏まえると、(unsafe コードを除く)「安全な」Rust では、データ競合 を完全に回避できる ことが分かる
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目